/*
 * Copyright (c) 2017, The Easegress Authors
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Package httpstat implements the statistics tool for HTTP traffic.
package httpstat

import (
	"math"
	"sync"
	"sync/atomic"
	"time"

	metrics "github.com/rcrowley/go-metrics"

	"github.com/megaease/easegress/v2/pkg/util/codecounter"
	"github.com/megaease/easegress/v2/pkg/util/easemonitor"
	"github.com/megaease/easegress/v2/pkg/util/sampler"
)

type (
	// HTTPStat is the statistics tool for HTTP traffic.
	HTTPStat struct {
		mutex sync.RWMutex

		count  uint64
		rate1  metrics.EWMA
		rate5  metrics.EWMA
		rate15 metrics.EWMA

		errCount  uint64
		errRate1  metrics.EWMA
		errRate5  metrics.EWMA
		errRate15 metrics.EWMA

		total uint64
		min   uint64
		max   uint64

		durationSampler *sampler.DurationSampler

		reqSize  uint64
		respSize uint64

		cc *codecounter.HTTPStatusCodeCounter
	}

	// Metric is the package of statistics at once.
	Metric struct {
		StatusCode int
		Duration   time.Duration
		ReqSize    uint64
		RespSize   uint64
	}

	// RequestMetric contains request metrics.
	RequestMetric struct {
		Count uint64  `json:"count"`
		M1    float64 `json:"m1"`
		M5    float64 `json:"m5"`
		M15   float64 `json:"m15"`

		ErrCount uint64  `json:"errCount"`
		M1Err    float64 `json:"m1Err"`
		M5Err    float64 `json:"m5Err"`
		M15Err   float64 `json:"m15Err"`

		M1ErrPercent  float64 `json:"m1ErrPercent"`
		M5ErrPercent  float64 `json:"m5ErrPercent"`
		M15ErrPercent float64 `json:"m15ErrPercent"`

		Min  uint64 `json:"min"`
		Max  uint64 `json:"max"`
		Mean uint64 `json:"mean"`

		P25  float64 `json:"p25"`
		P50  float64 `json:"p50"`
		P75  float64 `json:"p75"`
		P95  float64 `json:"p95"`
		P98  float64 `json:"p98"`
		P99  float64 `json:"p99"`
		P999 float64 `json:"p999"`

		ReqSize  uint64 `json:"reqSize"`
		RespSize uint64 `json:"respSize"`
	}

	// StatusCodeMetric is the metrics of http status code.
	StatusCodeMetric struct {
		Code  int    `json:"code"`
		Count uint64 `json:"cnt"`
	}

	// Status contains all status generated by HTTPStat.
	Status struct {
		RequestMetric
		Codes map[int]uint64 `json:"codes"`
	}
)

func (m *Metric) isErr() bool {
	return m.StatusCode >= 400
}

// New creates an HTTPStat.
func New() *HTTPStat {
	hs := &HTTPStat{
		rate1:  metrics.NewEWMA1(),
		rate5:  metrics.NewEWMA5(),
		rate15: metrics.NewEWMA15(),

		errRate1:  metrics.NewEWMA1(),
		errRate5:  metrics.NewEWMA5(),
		errRate15: metrics.NewEWMA15(),

		min:             math.MaxUint64,
		durationSampler: sampler.NewDurationSampler(),

		cc: codecounter.New(),
	}

	return hs
}

// Stat stats the ctx.
func (hs *HTTPStat) Stat(m *Metric) {
	// Note: although this is a data update operation, we are using the RLock here,
	// which means goroutines can execute this function concurrently, and contentions
	// are handled by the atomic operations for each item.
	//
	// This lock is only a mutex for the 'Status' function below.
	hs.mutex.RLock()
	defer hs.mutex.RUnlock()

	atomic.AddUint64(&hs.count, 1)
	hs.rate1.Update(1)
	hs.rate5.Update(1)
	hs.rate15.Update(1)

	if m.isErr() {
		atomic.AddUint64(&hs.errCount, 1)
		hs.errRate1.Update(1)
		hs.errRate5.Update(1)
		hs.errRate15.Update(1)
	}

	duration := uint64(m.Duration.Milliseconds())
	atomic.AddUint64(&hs.total, duration)
	for {
		min := atomic.LoadUint64(&hs.min)
		if duration >= min {
			break
		}
		if atomic.CompareAndSwapUint64(&hs.min, min, duration) {
			break
		}
	}
	for {
		max := atomic.LoadUint64(&hs.max)
		if duration <= max {
			break
		}
		if atomic.CompareAndSwapUint64(&hs.max, max, duration) {
			break
		}
	}

	hs.durationSampler.Update(m.Duration)

	atomic.AddUint64(&hs.reqSize, m.ReqSize)
	atomic.AddUint64(&hs.respSize, m.RespSize)

	hs.cc.Count(m.StatusCode)
}

// Status returns HTTPStat Status, It assumes it is called every five seconds.
// https://github.com/rcrowley/go-metrics/blob/3113b8401b8a98917cde58f8bbd42a1b1c03b1fd/ewma.go#L98-L99
func (hs *HTTPStat) Status() *Status {
	hs.mutex.Lock()
	defer hs.mutex.Unlock()

	hs.rate1.Tick()
	hs.rate5.Tick()
	hs.rate15.Tick()
	hs.errRate1.Tick()
	hs.errRate5.Tick()
	hs.errRate15.Tick()

	m1, m5, m15 := hs.rate1.Rate(), hs.rate5.Rate(), hs.rate15.Rate()
	m1Err, m5Err, m15Err := hs.errRate1.Rate(), hs.errRate5.Rate(), hs.errRate15.Rate()
	m1ErrPercent, m5ErrPercent, m15ErrPercent := 0.0, 0.0, 0.0
	if m1 > 0 {
		m1ErrPercent = m1Err / m1
	}
	if m5 > 0 {
		m1ErrPercent = m5Err / m5
	}
	if m15 > 0 {
		m1ErrPercent = m15Err / m15
	}

	percentiles := hs.durationSampler.Percentiles()
	hs.durationSampler.Reset()

	codes := hs.cc.Codes()
	hs.cc.Reset()

	mean, min := uint64(0), uint64(0)
	if hs.count > 0 {
		mean = hs.total / hs.count
		min = hs.min
	}
	status := &Status{
		RequestMetric: RequestMetric{
			Count: hs.count,
			M1:    m1,
			M5:    m5,
			M15:   m15,

			ErrCount: hs.errCount,
			M1Err:    m1Err,
			M5Err:    m5Err,
			M15Err:   m15Err,

			M1ErrPercent:  m1ErrPercent,
			M5ErrPercent:  m5ErrPercent,
			M15ErrPercent: m15ErrPercent,

			Min:  min,
			Mean: mean,
			Max:  hs.max,

			P25:  percentiles[0],
			P50:  percentiles[1],
			P75:  percentiles[2],
			P95:  percentiles[3],
			P98:  percentiles[4],
			P99:  percentiles[5],
			P999: percentiles[6],

			ReqSize:  hs.reqSize,
			RespSize: hs.respSize,
		},

		Codes: codes,
	}

	return status
}

// ToMetrics implements easemonitor.Metricer.
func (s *Status) ToMetrics(service string) []*easemonitor.Metrics {
	results := make([]*easemonitor.Metrics, 0, 32)

	results = append(results, &easemonitor.Metrics{
		CommonFields: easemonitor.CommonFields{
			Service: service,
			Type:    "eg-http-request",
		},
		OtherFields: &s.RequestMetric,
	})

	for code, count := range s.Codes {
		results = append(results, &easemonitor.Metrics{
			CommonFields: easemonitor.CommonFields{
				Service: service,
				Type:    "eg-http-status-code",
			},
			OtherFields: &StatusCodeMetric{
				Code:  code,
				Count: count,
			},
		})
	}

	return results
}
